Skip to content

ElasticSearch 全文检索:倒排索引表 + IK 分词器 + BM25 算法

前面学了基于向量数据库 Milvus 实现 RAG 语义检索,并且基于 LangGraph 实现了闭环的 Agentic RAG——也就是 Agent 自主决策要不要检索、用什么检索、信息够不够、效果怎么样、要不要重新搜。具体的 Agentic RAG 要根据业务场景设计,理解这个闭环的思路就行。

但向量检索有个问题:

  • 专业术语、精确实体更适合关键词检索,纯语义检索容易匹配不准

解决方案是:

  • 同时结合关键词检索与语义检索,由模型统一融合多路结果,提升专业场景准确率

这里关键词检索用 ElasticSearch 中间件,它是专门用来实现全文检索的。

一句话定位:数据库是根,而中间件是特种兵。我们会把原始数据存 MySQL,把需要检索的部分同步到 ES 里,这样关键词检索就可以走 ES 了。

我们这节学一下 ElasticSearch 全文检索的中间件。

一、用 Docker Compose 安装 ES + Kibana

用上节学的 docker compose 的方式安装(之前安装 Milvus 也是这样)。

创建目录:

bash
mkdir es-test
cd es-test
npm init -y

添加 docker-compose.yml

yaml
version: '3.8'
services:
  # Elasticsearch 最新稳定版:8.17.0
  es:
    image: elasticsearch:8.17.0
    container_name: es-dev
    ports:
      - "9200:9200"   # ES 对外提供服务的端口
    environment:
      - discovery.type=single-node        # 单节点运行(开发环境)
      - xpack.security.enabled=false      # 关闭安全认证,免密码访问
      - xpack.security.http.ssl.enabled=false
      - xpack.security.transport.ssl.enabled=false
      - ES_JAVA_OPTS=-Xms512m -Xmx512m    # JVM 内存配置,避免占用过高
    volumes:
      - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/es:/usr/share/elasticsearch/data
    restart: always
  # Kibana 最新稳定版:8.17.0(必须与 ES 版本完全一致)
  kibana:
    image: kibana:8.17.0
    container_name: kibana-dev
    ports:
      - "5601:5601"   # Kibana 网页控制台端口
    environment:
      - ELASTICSEARCH_HOSTS=http://es:9200   # 连接 ES 容器内部地址
    volumes:
      - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/kibana:/usr/share/kibana/data
    restart: always
    depends_on:
      - es   # 等待 ES 启动完成后再启动 Kibana
networks:
  default:
    name: common-network

Kibana 是 ES 的一个可视化控制台。跑一下:

bash
docker compose up -d

二、ES 基础:索引与文档

MySQL 是表和行,在 ES 里就是索引(index)和文档(document)。ES 没有 SQL,都是一些 HTTP 的接口(我们下面用 Kibana 的 Dev Tools 写这些 HTTP 请求)。

我们试一下索引的创建和文档的增删改查。

索引的创建与查看

http
# 1. 查看所有索引
GET /_cat/indices?v&h=health,status,index,docs.count

# 2. 创建索引
PUT /article
{
  "mappings": {
    "properties": {
      "title":      { "type": "text" },
      "content":    { "type": "text" },
      "author":     { "type": "keyword" },
      "createTime": { "type": "date" },
      "viewCount":  { "type": "integer" }
    }
  }
}

# 3. 查看索引结构
GET /article/_mapping

# 4. 查看索引配置
GET /article/_settings

# 5. 删除索引
DELETE /article

注意字段类型:text 会被分词、可用于全文检索;keyword 不分词、用于精确匹配(如作者、分类)。

文档的增删改查

http
# 1. 新增文档(自动生成 ID)
POST /article/_doc
{
  "title": "Elasticsearch 全文检索入门",
  "content": "ES 基于倒排索引与 BM25 实现全文搜索,适用于文本检索场景",
  "author": "后端开发",
  "createTime": "2026-04-26",
  "viewCount": 128
}

# 2. 新增文档(指定自定义 ID)
PUT /article/_doc/1001
{
  "title": "RAG 混合检索实战",
  "content": "ES 负责关键词检索,Milvus 负责向量语义检索,结合使用效果更佳",
  "author": "AI 开发",
  "createTime": "2026-04-26",
  "viewCount": 256
}

# 3. 根据 ID 查询单条
GET /article/_doc/1001

# 4. 查询全部文档
GET /article/_search
{ "query": { "match_all": {} } }

# 5. 全文分词检索(text 字段)
GET /article/_search
{ "query": { "match": { "content": "RAG 向量 检索" } } }

# 6. 精确匹配查询(keyword 字段)
GET /article/_search
{ "query": { "term": { "author": "AI 开发" } } }

# 7. 只返回指定字段
GET /article/_search
{ "_source": ["title", "author"], "query": { "match_all": {} } }

# 8. 分页 + 排序
GET /article/_search
{
  "from": 0, "size": 10,
  "sort": [{ "viewCount": "desc" }],
  "query": { "match_all": {} }
}

# 9. 局部更新文档(推荐)
POST /article/_update/1001
{ "doc": { "viewCount": 999, "title": "RAG 混合检索高级实战" } }

# 10. 全量覆盖更新
PUT /article/_doc/1001
{
  "title": "全量覆盖测试", "content": "原始内容被替换",
  "author": "测试用户", "createTime": "2026-04-26", "viewCount": 66
}

# 11. 根据 ID 删除文档
DELETE /article/_doc/1001

# 12. 条件批量删除
POST /article/_delete_by_query
{ "query": { "match": { "author": "后端开发" } } }

# 13. 统计文档总数
GET /article/_count

# 14. 清空索引数据(保留表结构)
POST /article/_delete_by_query
{ "query": { "match_all": {} } }

我们测了一遍索引的创建、文档的增删改查,整体比较简单。

三、为什么要用 ES?倒排索引

那用 ES 和之前用 MySQL 比有啥好处呢?ES 相比 MySQL 最大的核心优势,本质来源于倒排索引的底层设计。

普通 MySQL 使用的是正向索引:以一行为单位存储完整数据,检索文本内容时,需要逐行遍历、逐个字段匹配内容。数据量越大、文本越长,模糊 / 全文搜索就越慢,性能极差,并不适合大范围关键词检索。

而 Elasticsearch 采用倒排索引机制:会自动对 text 类型字段进行分词处理,拆解为一个个独立词条,再以「词条」为核心,反向关联所有包含该词条的文档。

简单来说:

  • 正向索引:文档 → 关键词
  • 倒排索引:关键词 → 文档

基于这种结构,用户输入关键词检索时,ES 只需通过词条快速匹配对应的文档,无需全表遍历,就能实现海量文本下毫秒级的全文检索。

综上,ES 倒排索引的底层架构,就是专门为海量文本、关键词模糊检索、内容匹配场景量身设计的,这也是它吊打 MySQL 全文搜索的根本原因。理解了倒排索引,就理解了 ES 了。

四、中文分词:IK 分词器

显然,在 ES 里分词是很重要的——不同的分词建的索引表都不同。

ES 默认的 standard 分词器对中文支持不好:

http
POST /_analyze
{
  "analyzer": "standard",
  "text": "Elasticsearch RAG 混合检索知识库"
}

它是每个字单独拆开的,而实际上应该"混合"、"检索"分别是整体来建立索引。这就需要用到 IK 分词器了。

安装 IK 分词器

创建 elasticsearch/Dockerfile

dockerfile
# 官方 ES 基础镜像
FROM elasticsearch:8.17.0
# 安装 IK 分词(版本严格和 ES 一致)
RUN elasticsearch-plugin install --batch \
    https://release.infinilabs.com/analysis-ik/stable/elasticsearch-analysis-ik-8.17.0.zip

就是在 ES 的容器里,执行命令安装 IK 分词器插件。我们用这个 Dockerfile 来构建 ES 镜像,改一下 docker-compose.yml

yaml
# Elasticsearch 8.17.0 + IK 中文分词(内置到镜像)
es:
  build: ./elasticsearch        # 从本地 Dockerfile 构建镜像(自带 IK)
  container_name: es-dev
  ports:
    - "9200:9200"
  environment:
    - discovery.type=single-node
    - xpack.security.enabled=false
    - xpack.security.http.ssl.enabled=false
    - xpack.security.transport.ssl.enabled=false
    - ES_JAVA_OPTS=-Xms512m -Xmx512m
  volumes:
    - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/es/data:/usr/share/elasticsearch/data
  restart: always

重新构建并启动:

bash
docker compose down
docker compose up -d --build

验证:

http
# 1. 检查 ES 状态
GET /

# 2. 查看已安装插件
GET /_cat/plugins?v

# 3. 原生 standard 分词
POST /_analyze
{ "analyzer": "standard", "text": "Elasticsearch RAG 混合检索知识库" }

# 4. IK 细粒度分词(索引入库用)
POST /_analyze
{ "analyzer": "ik_max_word", "text": "Elasticsearch RAG 混合检索知识库" }

# 5. IK 智能分词(搜索查询用)
POST /_analyze
{ "analyzer": "ik_smart", "text": "Elasticsearch RAG 混合检索知识库" }

IK 分词器有两种模式:

  • ik_max_word(细粒度):把文本尽可能拆成最多的词,用于索引入库(比如"知识库"会进一步细分为"知识"、"库"),召回率高
  • ik_smart(智能/粗粒度):只拆出最关键、不重叠的词,用于搜索查询,效率高

有了 IK 分词器之后,就可以快速地查询关键词对应的文档了。之前(standard)是把搜索词分词后去索引表匹配,但默认分词器拆太细,索引的意义不大;用了 IK 分词器之后,按照中文的词语来分词建立倒排索引表,查询时也用分词器分词、查索引表、结果合并后返回——这样的机制,自然可以毫秒级实现关键词检索。

IK 双分词的实际索引

之前创建 article 的索引用的是默认的分词器。这次我们用 ik_max_word 来做索引生成(更细粒度),用 ik_smart 来做检索的分词。关键就是在字段上同时配 analyzer(入库分词)和 search_analyzer(查询分词):

http
# Elasticsearch IK 分词版操作手册
# 索引:life_note
# 字段全部配置 IK 分词:入库 ik_max_word / 查询 ik_smart

# 1. 创建索引(生活笔记场景 + IK 双分词)
PUT /life_note
{
  "mappings": {
    "properties": {
      "title":   { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" },
      "content": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" },
      "type":    { "type": "keyword" },
      "author":  { "type": "keyword" },
      "record_time": { "type": "date" }
    }
  }
}

# 2. 查看索引结构
GET /life_note/_mapping

# 3. 查看索引配置
GET /life_note/_settings

# 4. 删除索引
DELETE /life_note

文档的增删改查和前面 article 基本一致,只是字段换成了生活笔记场景(title/content/type/author/record_time),检索示例:

http
# 全文分词检索(IK 中文分词,搜:健康 作息 旅行)
GET /life_note/_search
{ "query": { "match": { "content": "健康 作息 旅行" } } }

# 精确匹配查询(keyword 分类字段)
GET /life_note/_search
{ "query": { "term": { "type": "健康生活" } } }

# 分页 + 时间排序
GET /life_note/_search
{
  "from": 0, "size": 10,
  "sort": [{ "record_time": "desc" }],
  "query": { "match_all": {} }
}

试一下,我们就基于 IK 分词器实现了中文的关键词检索。

五、BM25 算法:检索结果怎么排序

学习 ElasticSearch 还要知道一个 BM25 的算法。虽然用 IK 分词器分词后,建立倒排索引表,然后就可以检索了。但是检索到的文档相关性怎么样,如何排序?这就是 BM25 算法做的。

BM25(Best Matching 25)是全文检索的相关性打分算法。它有一些策略,比如:

  • 词频饱和:词出现次数到一定程度,分数不再涨(防堆砌)
  • 文档长度归一化:越长的文档,越"扣分"(公平对比长短文)
  • 稀有词权重高:少见词(如"IK 分词")比常用词(如"的")更重要

总之,BM25 算法是公平、高效、稳定的关键词排序算法,ES 默认用它,后面 RAG 做关键词检索也是依赖这个。

理解了 IK 分词器 + 倒排索引 + BM25 算法,就理解了 ElasticSearch 了。

六、总结

这节我们学了 ElasticSearch 做全文检索。用 docker compose 安装了 ES 和它的可视化控制台 Kibana。

ES 里只有索引、文档这两层。我们通过 HTTP 的接口创建了索引、文档,做了文档的增删改查。

ES 的检索原理就是倒排索引,也就是关键词 → 文档的索引表,这是它能实现海量文档毫秒级检索的核心:

  • 入库的时候会对类型为 text 的字段分词,放到倒排索引表(类型为 keyword 的字段不会)
  • 检索的时候会把 query 也分词,分别检索倒排索引表,把查到的文档合并返回
  • 结果还会用 BM25 算法来排序

中文分词我们安装了 IK 分词器,入库用 ik_max_word 细粒度分词,检索用 ik_smart 高效分词。

理解了倒排索引表 + BM25 算法 + IK 分词器,串起来就理解了 ElasticSearch 的核心了。

下一篇就是混合检索 RAG:把 ES 的关键词检索和 Milvus 的向量语义检索多路召回,再用重排模型融合——这正是前面 Agentic RAG 里"结合关键词与语义检索"那句话的工程落地。

延伸思考(评论区精选)

  • 电商关键词搜索怎么把 MySQL 数据同步到 ES? 有读者问:实现电商平台的搜索,是不是要把 MySQL 的数据先按一定结构 insert 到 ES?作者答:只有需要搜索的内容放 ES,再加一个数据库表里对应的 id 就行——从 ES 查出来后拿 id 去 MySQL 找具体数据。也就是"核心搜索关键词用 ES 搜,存一份 SQL 的关联 ID,需要的话在 SQL 查"。常规做法是用同步组件(如 Logstash / Canal / 应用双写)保持两边一致,避免全量搬数据。
  • 能不能不用 ES? 有读者提到"pg 也有 BM25 的插件了(pg_search / pg_bm25),所以可以不⽤ es 了"。作者补充:数据量大了还是得 ES——ES 在海量文档、高并发检索上的工程成熟度还是更稳。这和前面 Agentic RAG 评论区"中小企业用 PostgreSQL 够不够"的讨论可以对照看。
  • Kibana 连不上 ES? 有读者反馈 ES 和 Kibana 都显示启动成功,但浏览器打开 Kibana 一直转圈连不上。作者判断:应该是磁盘存储空间不够了,清理一下即可(ES 默认当磁盘使用率过高时会限制写入甚至拒绝服务)。
  • 文章里的操作手册(索引/文档 CRUD、IK 分词测试)都建议从课程仓库复制,避免手敲出错。

补充:做 RAG 时怎么选数据库?(MySQL / ES / 向量库)

学完 ES 之后,一个很自然的问题浮出来了:平时做东西,MySQL、ES、向量数据库这三个,我们是都要有,还是只要 ES + 向量库就行?做 RAG 通常怎么选?MySQL 还是必须的吗?

先给结论:这三个不是"三选几"的关系,而是各管一类访问模式、互补的。是否全要,取决于你的数据和查询长什么样。

三个数据库各自的角色

数据库擅长什么在 RAG 里的位置
MySQL / Postgres业务结构化数据、事务、增删改查、join、精确更新系统 of record(权威源),不是检索组件
向量库(Milvus / pgvector)语义相似检索——"意思接近"的文档语义召回那一半
ES关键词 / 全文精确检索——精确词、术语、ID、短语,BM25 排序关键词召回那一半

关键认知:向量库和 ES 解决的是"怎么把相关文档找出来",MySQL 解决的是"我的核心业务数据存在哪、怎么管"。它们是不同层的事。

做 RAG 时通常怎么选

按检索强度分三档:

  1. 纯语义 RAG(demo / 简单场景):只要向量库。像前面几篇 demo,小说切片只进 Milvus,根本没碰 MySQL/ES。
  2. 生产级混合 RAG向量库 + ES 双路召回。同一份源文档同步成两份索引——一份 embedding 进向量库(语义),一份分词进 ES(关键词),查询时两路都查、重排融合。这正是 Agentic RAG 里"结合关键词与语义检索"的工程落地。
  3. 带业务系统的 RAG:在上面基础上,源文档和业务数据还放在 MySQL / 对象存储里当权威源。检索命中后,用文档里带的 id 回 MySQL 取完整/最新数据。

MySQL 还是必须的吗?

分两层看:

  • 对"检索"本身:不是必须。检索只需要向量库(关键词弱一点就加 ES)。
  • 对"整个应用":通常还是要有个系统 of record。但如果你的 RAG 只是检索一堆静态文档(比如公司内部手册、小说),完全可以不要 MySQL,文档放对象存储或向量库的 metadata 字段即可。课程 demo 就是这么干的。

现实工程里更常见的省心做法:直接用 Postgres 一把梭——关系表 + pgvector(向量)+ pg_search(BM25 关键词)。这样不单独养 Milvus 和 ES,运维成本低,中小企业/创业项目完全够用(呼应上面评论区"pg 也有 BM25 插件""中小企业用 PostgreSQL 足够"的讨论)。只有当数据量很大、并发很高时,才值得把 Milvus / ES 拆出来当专用中间件。

一句话决策建议

先问自己两件事:① 我的核心业务数据要事务和结构化查询吗? 要 → 必有 MySQL/Postgres。② 我需要多少种"找文档"的方式? 只要语义 → 向量库;要专业术语/精确匹配也准 → 再加 ES。三者不是都要,但"系统 of record + 至少一种检索"这个组合基本跑不掉。

预览到此为止,输入密码解锁全文

解锁后本机会记住,同密码的其他文章也无需重复输入

基于 VitePress 构建 · 专注前端与 AI 实战